
Alkaline.Modules.newModule('Documentation', function() {

//this.moduleType = Alkaline.MODULE_TYPES.OUTPUT;
this.requiredModules = [{ name: 'Analyzer' }];

this.registerSupportedArgument('classesMaySkipPrototype', 'cmsp', 1, false);

var TOKEN = Alkaline.JSInfo.TOKEN;

this.run = function(ast) {
	this.setup();
	this.DFS(ast, [ TOKEN.Operator, TOKEN.Comment ], this.processOperatorOrModule);
	if (!this.arguments.classesMaySkipPrototype)
		this.postprocessItemsFromThis();
	this.DFS(ast, [ TOKEN.Comment ], this.processComment);
	this.root.completeDoc();
}

this.setup = function() {
	this.root = new this.Item(null);
	this.builder = this.compileResult.treeBuilder;
	this.root.builder = this.builder;
	this.currentModule = null;
	this.modules = {};
	this.itemsFromThis = [];
}

this.postprocessItemsFromThis = function() {
	var items = this.itemsFromThis;
	nextItem:
	for (var i = 0; i < items.length; i++)
		if (items[i].fromThis) {
			var parent = items[i].parent;
			if (parent.name !== 'prototype')
				continue;

			for (var j = 0; j < parent.list.length; j++)
				if (!parent.list[j].fromThis) {
					for (var k = 0; k < parent.list.length; k++)
						if (parent.list[k].fromThis)
							parent.list[k].fromThis = null;
					continue nextItem;
				}
			
			// remove "prototype", <this> must reffer to the namespace and not the class
			var realParent = parent.parent.parent;
			for (var j = 0; j < parent.list.length; j++) {
				parent.list[j].fromThis = null;	// do not to this again for items from the same function
				if (!realParent.byName[parent.list[j].name]) {
					// move child into realParent
					var item = this.getItemAtPath(parent.list[j].name, realParent);
					item.isFunction = items[i].isFunction;
					item.fromThis = null;
					item.refferences = items[i].refferences;
					for (var k = 0; k < item.refferences.length; k++)
						item.refferences[k].itemAtPath = item;
				}
			}
			// remove mistaken "prototype"
			parent.parent.list.splice(parent.parent.list.indexOf(parent), 1);
			delete parent.parent.byName.prototype;
		}
}



this.Item = function(name, parent) {
	this.list = [];
	this.byName = {};
	this.name = name;
	this.parent = parent;
	this.fullName = name;
	this.isStatic = name !== 'prototype';
	this.refferences = [];
	if (parent) {
		this.builder = parent.builder;
		this.isStatic = this.isStatic && parent.isStatic;
		if (parent.fullName)
			this.fullName = parent.fullName + '.' + name;
		this.propertyDescriptor = new Alkaline.Processing.PropertyDescriptor(parent, name);
		this.propertyDescriptor.setDocComment(this.builder.Comment('/**\n*/'));
	}
}
this.Item.prototype.constructor = this.Item;
this.Item.prototype.getItem = function(name, alreadyDeclared) {
	if (!(this.byName[name] instanceof this.constructor)) {
		this.list.push(this.byName[name] = new this.constructor(name, this));
		this.byName[name].implied = alreadyDeclared;
	} else if (!alreadyDeclared && this.byName[name].implied)
		this.byName[name].implied = false;
	return this.byName[name];
}
this.Item.prototype.isClass = function() {
	return !!this.byName.prototype;
}
this.Item.prototype.isStatic = function() {
	var item = this;
	while (item && item.name !== 'prototype')
		item = item.parent;
	if (item)
		return false;
	return true;
}
this.Item.prototype.completeDoc = function() {
	if (this.name)
		this._completeDoc();
	
	for (var i = 0; i < this.list.length; i++)
		this.list[i].completeDoc();
}
this.Item.prototype._completeDoc = function() {
	var type = '';
	if (this.parent.name === 'prototype') {
		// we are inside a class
		if (this.isClass()) {
			this.propertyDescriptor.docDescription += '\n@for ' + this.parent.parent.fullName;
			type = 'class';
		} else if (this.propertyDescriptor.docDescription.contains('@event'))
			type = 'event';
		else if (this.isFunction)
			type = 'method';
		else
			type = 'property';
	} else if (this.isClass()) {
		type = 'class';
	}
	if (this.isStatic) {
		if (!this.isClass())
			this.propertyDescriptor.docDescription += '\n@static';
		if (this.parent && this.parent.name)
			this.propertyDescriptor.docDescription += '\n@namespace ' + this.parent.fullName;
	}
	if (type) {
		this.propertyDescriptor.docDescription += '\n@' + type + ' ' + this.name;
		if (type === 'class')
			this.propertyDescriptor.docDescription += '\n@constructor';
	}
}


this.processOperatorOrModule = function(ast) {
	if (ast.token.type === TOKEN.Operator)
		return this.processOperator(ast);
	else {
		var module = ast.token.text.match(/@module\s+(\w+)/);
		if (module) {
			var name = module[1];
			this.currentModule = name;
			if (!this.modules[name])
				this.modules[name] = {
					description: name + ' module'
				};
			var desc = ast.token.text;
			desc = desc.substr(0, module.index) + desc.substr(module.index + module[0].length);
			desc = Alkaline.JSInfo.getCommentContent(desc);
			if (desc)
				this.modules[name].description = desc;
		}
	}
}


this.processOperator = function(ast) {
	if (ast.token.text !== '=')
		// not an assignment
		return;
	var lhs = ast.children[0];
	var path = [];
	while (lhs.token.type === TOKEN.SELECT && lhs.children[1].token.type === TOKEN.String) {
		path.push(Alkaline.JSInfo.unescapeString(lhs.children[1].token.text));
		lhs = lhs.children[0];
	}
	if (lhs.token.type !== TOKEN.Id)
		// not interested; could be a call or an expression inside the selector: a().b = c; a[b()] = c;
		return;
	var fromThis = false;
	if (lhs.token.text !== 'this') {
		if (lhs.objectValue && lhs.objectValue.lexicalContext && lhs.objectValue.lexicalContext.inheritedContext)
			// local variable in a function; ignore
			return;
		path.push(lhs.token.text);
		path.reverse();
	} else {
		var parent = ast.parent;
		while (parent && !parent.assignedToPath && parent.token.type !== TOKEN.Function)
			parent = parent.parent;
		if (parent && !parent.assignedToPath) {
			// it must be a function
			parent = parent.parent;
			if (!parent || parent.token.text !== '=' || !parent.assignedToPath)
				// unknown closure
				return;
		}
		path.reverse();
		if (parent) {
			var parentPath = parent.assignedToPath;
			var protoIdx = parentPath.indexOf('prototype');
			if (protoIdx > -1)
				parentPath = parentPath.slice(0, protoIdx);
			path = parentPath.concat(['prototype'].concat(path));
			fromThis = true;
		}
	}
	ast.assignedToPath = path;
	ast.assignedTo = path.join('.');
	var isFunction = false;
	var fct = ast.children[1];
	while (fct.token.type === TOKEN.Operator && fct.token.text === '=')
		fct = fct.children[1];
	if (fct.token.type === TOKEN.Function || (fct.token.type === TOKEN.CALL && fct.children[0].token.type === TOKEN.SELECT && ['bind', 'bindAsEventListener'].contains(fct.children[0].children[1].token.text)))
		isFunction = true;
	var item = ast.itemAtPath = this.getItemAtPath(path);
	item.isFunction = isFunction;
	item.refferences.push(ast);
	if (!('fromThis' in item)) {
		item.fromThis = fromThis;
		if (fromThis)
			this.itemsFromThis.push(item);
	} else
		item.fromThis = item.fromThis && fromThis;
}

this.processComment = function(ast) {
	if (ast.parent.children.length > ast.childIndex+1 && !ast.token.text.contains('@module')) {
		var nextSibling = ast.parent.children[ast.childIndex+1];
		while (nextSibling.token.additionalCommentInfo) {
			if (ast.parent.children.length === nextSibling.childIndex+1)
				return;	// there is no content after this comment
			nextSibling = ast.parent.children[nextSibling.childIndex+1];
		}
		
		var item = nextSibling.itemAtPath;
		if (!item) return;
		
		if (ast.token.additionalCommentInfo) {
			var extraPath = [], m;
			for (var i = 0; i < ast.token.additionalCommentInfo.length; i++) {
				var str = ast.token.additionalCommentInfo[i];
				if (m = str.match(/^object part name: (.+)$/))
					extraPath.push(Alkaline.JSInfo.unescapeString(m[1]));
			}
			
			item = this.getItemAtPath(extraPath.reverse(), item);
			item.isFunction = false;
			item.fromThis = false;
		}
		
		item.propertyDescriptor.setDocComment(ast);
	}
}

this.getItemAtPath = function(path, root) {
	var item = root || this.root;
	if (typeof path === 'string')
		path = [path];
	for (var i = 0; i < path.length; i++) {
		item = item.getItem(path[i], i < path.length-1, this.currentModule);
		if (!('module' in item))
			item.module = this.currentModule;
	}
	return item;
}

});
